最简单说清楚Springboot中的bean加载顺序

您所在的位置:网站首页 spring bean注入顺序 最简单说清楚Springboot中的bean加载顺序

最简单说清楚Springboot中的bean加载顺序

2024-07-12 18:24| 来源: 网络整理| 查看: 265

前言

前天一个单身同事说他脱单了,我问他女方是谁,哪里认识的,他不说,装高人,许久才告诉我说,他的对象是注入的,我悟了许久,原来是公司里面的女同事呀,我又问他我认不认识,他又不说,装大佬,许久才让我想想Springboot里面的bean的加载顺序以及我啥时候入职的,在那一刻,我突然就紧张了。

我紧张,不是因为我猜到了他对象是谁,而是我好像不是特别理得清Springboot里面的bean的加载顺序,为了缓解我的紧张感,我赶紧去做了一下功课,才发现原来之前有写过一篇文章就能解释清楚这个事情,所以我拜读了自己曾经的作品后,结合了一个很哇塞的示例,想在这里再单独对Springboot里面的bean的加载顺序,进行一个梳理。

Springboot版本:2.7.6

正文 一. Bean的前世今生

通常说到Spring里面的bean,大家都有自己的认知,按照大家的认知,可以把大家分类如下。

一年大头兵。知道bean就是Spring为我们创建的一个对象; 二年大头兵。不但知道一年大头兵知道的一切,还知道bean在被创建前,还经历过BeanDefinition; 三年大头兵。不但知道二年大头兵知道的一切,还知道bean在成为BeanDefinition前,还经历过ConfigurationClass。

其实吧,通常Spring中的bean,在真正成为一个bean前,首先是会被解析为ConfigurationClass,然后再基于ConfigurationClass创建出BeanDefinition,最后在容器初始化的时候,再由BeanDefinition创建出我们的bean。

可能帅气的大聪明就有疑问了,我们关注bean的今生就好了,为什么要去了解bean的前世呢,确实,如果不想去理清bean的加载顺序,那么是不需要去关注bean的前世的,但是如果想理清bean的加载顺序,那么bean的前世就尤为重要了,特别是ConfigurationClass,搞明白ConfigurationClass怎么来的,再搞明白如何基于ConfigurationClass得到BeanDefinition,那bean的加载顺序就十分的清楚了,这在我之前的那篇文章中是解释得很清楚的,但是那篇文章写得太长了,我估摸很多帅比是耐不住性子看完的,所以我就将ConfigurationClass和BeanDefinition的加载用流程图的形式画了出来,我觉得帅比们肯定喜欢看图吧。

先看一下ConfigurationClass的加载流程图,如下所示。

Spring-ConfigurationClass加载流程图

可以发现把候选者创建为ConfigurationClass然后添加到configurationClasses中的过程是一个递归的过程,首先递归到的是初始配置对象,这个初始配置对象在Springboot中就是Springboot的启动类,然后由初始配置对象,开枝散叶,把各种其它的候选者的ConfigurationClass在递归的过程中都添加到configurationClasses中,同时结合上图可以发现,不同类型的候选者的ConfigurationClass添加到configurationClasses中是有先后顺序的,结合整个递归的流程,添加的先后顺序是如下的。

首先,由@Controller,@Service,@Repository,@Component和@Configuration注解修饰的对象,和@Import注解间接或直接导入的对象; 其次,由自动装配机制加载的配置对象。

因为configurationClasses是一个LinkedHashMap,所以后续在遍历configurationClasses时,先添加的会被先遍历到,这一点很重要。

现在拿到configurationClasses了,再往后就是遍历configurationClasses,然后解析每个ConfigurationClass并得到相关的BeanDefinition,流程图如下所示。

Spring-BeanDefinition加载流程图

上述流程就是按照ConfigurationClass添加到configurationClasses的先后顺序,依次遍历到每一个ConfigurationClass,然后会判断这个ConfigurationClass是否需要被跳过,这里判断的依据一般就是我们常使用的@ConditionalOnBean和@ConditionalOnMissingBean等Condition相关注解,再然后就是把ConfigurationClass自己注册为容器中的BeanDefinition和把ConfigurationClass对应的BeanMethod(由@Bean注解修饰的方法)注解为容器中的BeanDefinition,最后执行ConfigurationClass对应的ImportBeanDefinitionRegistrar的逻辑来向容器中注册BeanDefinition。

要特别关注,@ConditionalOnBean和@ConditionalOnMissingBean等Condition相关注解生效的时间,就是在将ConfigurationClass解析为BeanDefinition的一开始,那么ConfigurationClass被遍历到的顺序就尤为重要的,我们可以仔细想想,假设A添加了@ConditionalOnBean(B.class),再假设A的ConfigurationClass又先于B的ConfigurationClass被添加到configurationClasses中,那么A的ConfigurationClass就会被先遍历到,此时A的ConfigurationClass解析为BeanDefinition的一开始,就会去判断B的BeanDefinition是否存在于容器中,在我们的假设场景下此时B是不存在的,所以A的ConfigurationClass不会被加载为容器中的BeanDefinition,如果想让A的Condition被满足,就需要让B的ConfigurationClass先于A的被加载。

到此,bean的前世今生,就介绍完毕了,稍微总结一下。

bean的前世其实是BeanDefinition; BeanDefinition的前世其实是ConfigurationClass; 每个ConfigurationClass会按照加载的先后顺序依次解析为BeanDefinition; 每个ConfigurationClass在解析为BeanDefinition时会先进行各种Condition判断; 所以ConfigurationClass的加载顺序其实就影响bean的加载顺序。 由@Controller,@Service,@Repository,@Component和@Configuration注解修饰的对象,和@Import注解间接或直接导入的对象,其ConfigurationClass先加载; 自动装配机制加载的配置对象,其ConfigurationClass后加载; 每个ConfigurationClass包含着自己的BeanMethod(由@Bean注解修饰的方法)和ImportBeanDefinitionRegistrar(通过@Import注解导入的); ConfigurationClass在解析为BeanDefinition时,会遵循ConfigurationClass自己本身,BeanMethod,ImportBeanDefinitionRegistrar这样的顺序来解析。 二. Bean的加载示例

上面已经有理论知识了,下面来结合一个示例,来做一个验证。

示例工程结构如下所示。

bean加载顺序示例工程结构图

further包下面的代码如下所示。

@ConditionalOnBean(MyController.class) public class MyFurtherConfig { public MyFurtherConfig() { System.out.println(); } @Bean public MyFurtherService myFurtherService() { return new MyFurtherService(); } } public class MyFurtherService { public MyFurtherService() { System.out.println(); } }

origin包下面的代码,如下所示。

@Configuration @Import({MyImportBeanDefinitionRegistrar.class, MyImportSelector.class, MySupport.class}) public class MyConfig { public MyConfig() { System.out.println(); } @Bean public MyDao myDao() { return new MyDao(); } } public class MyController { public MyController() { System.out.println(); } } public class MyDao { public MyDao() { System.out.println(); } } public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition beanDefinition = new RootBeanDefinition(MyController.class); registry.registerBeanDefinition("myController", beanDefinition); } } public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { "com.lee.ioc.order.further.MyFurtherConfig" }; } } @Service public class MyService { public MyService() { System.out.println(); } } public class MySupport { public MySupport() { System.out.println(); } }

pom文件如下所示。

4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.6 8 8 com.lee.ioc.order ioc-order 1.0-SNAPSHOT com.lee.ioc.order.starter ioc-order-starter 1.0-SNAPSHOT

示例工程引入了一个提前写好的starter,这个starter结构如下所示。

bean加载顺序示例starter工程结构图

TestBean和MyAutoConfig代码如下。

public class TestBean { } @Configuration public class MyAutoConfig { public MyAutoConfig() { System.out.println(); } @Bean public TestBean testBean() { return new TestBean(); } }

spring.factories文件内容如下。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.lee.ioc.order.starter.config.MyAutoConfig

pom文件如下所示。

4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.6 8 8 com.lee.ioc.order.starter ioc-order-starter 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-web org.apache.maven.plugins maven-source-plugin attach-sources jar

通过在每个bean的构造函数中打断点,可以知道这些bean的加载顺序,以debug运行程序,可以得到如下的加载顺序(越靠上越先实例化)。

MyConfig(由@Configuration注解修饰); MyService(由@Service注解修饰); MySupport(由@Import注解直接导入); MyDao(由@Bean注解修饰的方法导入); MyController(由ImportBeanDefinitionRegistrar导入) MyAutoConfig(由自动装配机制导入的配置对象); TestBean(由自动装配机制导入的配置对象里的@Bean注解修饰的方法导入)。

上述的结果是完全符合我们的理论推导的,但是好像漏了MyFurtherConfig和MyFurtherService,搞明白为什么漏了,就算是明白本文的主旨了。

首先,MyFurtherConfig是MyConfig通过@Import注解导入的ImportSelector间接导入的配置对象,所以按照ConfigurationClass的递归加载流程,我们可以确定,MyFurtherConfig的ConfigurationClass要先于MyConfig的ConfigurationClass加载。

其次,MyController是MyConfig通过@Import注解导入的ImportBeanDefinitionRegistrar在执行逻辑时会导入的对象,而只有在将MyConfig的ConfigurationClass解析为BeanDefinition时,才会执行到ImportBeanDefinitionRegistrar的逻辑。

所以,在将MyFurtherConfig的ConfigurationClass解析为BeanDefinition时,MyConfig的ConfigurationClass还没有被解析为BeanDefinition,从而对应的ImportBeanDefinitionRegistrar也没有被执行,因此MyController也没有被导入,最终导致MyFurtherConfig的@ConditionalOnBean(MyController.class)条件不满足,则MyFurtherConfig的ConfigurationClass解析为BeanDefinition的动作不会执行,进而也不会解析MyFurtherConfig的BeanMethod,现象就是MyFurtherConfig和MyFurtherService都没有注册到容器中。

总结

Spring或者说Springboot中的bean的加载顺序,是很容易被忽略的一个知识点,很多人也被那稍显复杂的源码劝退,从而只能去找相关的文章寻求答案,但很多讲bean加载顺序的文章,给的结论太过草率,结论其实是有误的,而要真正理解bean的加载顺序,其实是需要理解两大块内容。

ConfigurationClass的加载顺序。因为ConfigurationClass在解析为BeanDefinition时,会遵循先加载先解析的规则,所以在不加其它Condition条件时,先加载的ConfigurationClass,就是会比后加载的ConfigurationClass先一步执行到解析为BeanDefinition的逻辑; ConfigurationClass解析为BeanDefinition的步骤。ConfigurationClass会先把自己解析为BeanDefinition,然后如果有由@Bean注解修饰的方式,则把由@Bean注解对应的BeanDefinition解析出来,最后如果还通过@Import注解导入了ImportBeanDefinitionRegistrar,则再执行ImportBeanDefinitionRegistrar的逻辑来注册BeanDefinition。

所以bean的加载顺序是一个有迹可循但是容易让人晕乎的知识点,但如果能够理解上面的ConfigurationClass加载流程图和BeanDefinition加载流程图,这个知识点其实就搞定百分之九十了,剩下百分之十,无非就是各种Condition条件的使用和控制自动装配类加载顺序的注解的使用,不知道这些知识其实对整体的理解是不构成影响的。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3